/*****************************************************************************
 *   Copyright 2005 Tim A Wang
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 ****************************************************************************/
package com.jb.web.application;

import com.jb.faces.el.AttributeEvaluator;
import com.jb.framework.util.ExecutionException;
import com.jb.framework.util.PropUtil;
import com.jb.web.bean.ListBean;
import com.jb.web.bean.Page;
import com.jb.web.bean.Panel;
import com.jb.web.bean.Region;
import com.jb.web.bean.UIComponentImpl;
import com.jb.web.bean.event.AJaxEvent;
import com.jb.web.bean.event.DropElementEvent;
import com.jb.web.bean.event.UIEvent;
import com.jb.web.context.WebContext;
import com.jb.web.renderer.RenderKitImpl;
import com.jb.web.session.Session;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.el.ELException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.xml.sax.InputSource;


/**
 * Ajax server handles all AJax requests.
 * This is a immutate class, and should be thread safe.
 *
 * This is back end Ajax implemenation for jb component model. It allows
 * javascript client sends command.
 */
public class AjaxServer {
    private static Log log = LogFactory.getLog(AjaxServer.class.getName());
    private static String REQ_COMP_ID = "jbCompId";
    private static String RES_COMP_ID = "jbRespId";
    private static String RENDERED_PANEL = "AjaxServer.RENDERED_PANEL";
    private Map properties = 
        new HashMap(); //empty first to prevent null exception
    private String action;
    private String id;
    private String idType;

    /**
     * Returns action this request is about.
     *
     * @return action.
     */
    public String getAction() {
        return action;
    }

    /**
     *  Sets value property for a component. The request must pass
     *  id parameter, store client id of the component whose value is to be set.
     *  Optionally it can pass an id for component to render back.
     *  The value is passed as the requst stream. The client id is used to
     *  locate the component.
     * @param ctx
     * @param request
     * @param response
     */
    public void setComponentValue(WebContext ctx, HttpServletRequest request, 
                                  HttpServletResponse response) {
        UIComponentImpl component = getRequestComp(ctx, request);
        if (component != null) {
            try {
                String value = "";
                int lengthOfBuffer = request.getContentLength();
                InputStream input = request.getInputStream();
                byte[] buffer = new byte[lengthOfBuffer];

                ByteArrayOutputStream output = 
                    new ByteArrayOutputStream(lengthOfBuffer);
                int readBytes = -1;
                while ((readBytes = input.read(buffer, 0, lengthOfBuffer)) != 
                       -1) {
                    output.write(buffer, 0, readBytes);
                }
                value = 
                        new String(output.toByteArray(), request.getCharacterEncoding());
                //set component value, onValueChange event will be called.
                if (component instanceof ListBean) {
                    ((ListBean)component).setCellValues(ctx, value);
                } else
                    component.setValue(ctx, value);
                //render respond back
                UIComponentImpl responseComp = getResponseComp(ctx, request);
                if (responseComp != null) {
                    renderChildren(ctx, responseComp);
                }
            } catch (IOException e) {
                handleError(ctx, response, e);
            }
        } else {
            handleError(ctx, response, 
                        new Exception("Component not found " + id));
        }
    }

    /**
     * Component id for which this serverlet is handling.
     *
     * @return component id.
     */
    public String getId() {
        return id;
    }

    /**
     * Returns type of component id, either id, or client id.
     *
     * @return id type.
     */
    public String getIdType() {
        return idType;
    }

    public void handleError(WebContext ctx, HttpServletResponse response, 
                            Exception ex) {
        String errors = ctx.getAllErrors();
        if (log.isErrorEnabled()) {
            log.error("AJax request failed, client will refresh the page in none ajax way", 
                      ex);
            log.error(errors);
        }

        try {
            response.getWriter().write("aJaxError, refresh");
            response.getWriter().write(errors);

            if (ex != null) {
                ex.printStackTrace(response.getWriter());
            }
            response.getWriter().flush();
        } catch (IOException e) {
            if (log.isErrorEnabled())
                log.error(this, e);
        }
    }

    private UIComponentImpl getRequestComp(WebContext ctx, 
                                           HttpServletRequest request) {

        id = request.getParameter(REQ_COMP_ID);

        if ((id != null)) {
            UIComponentImpl page = (UIComponentImpl)ctx.getRequestTree();
            page.onRestored(ctx);

            UIComponentImpl component = getComponent(ctx, page, id);

            if (component != null) {
                ctx.setRequestComponent(component);
            }
            return component;
        } else {
            if (log.isErrorEnabled()) {
                log.error("Can not resolve request component id " + id);
            }
            return null;
        }
    }

    private UIComponentImpl getResponseComp(WebContext ctx, 
                                            HttpServletRequest request) {
        String id = request.getParameter(RES_COMP_ID);

        if ((id != null)) {
            UIComponentImpl page = (UIComponentImpl)ctx.getRequestTree();
            UIComponentImpl component = getComponent(ctx, page, id);
            return component;
        } else {
            if (log.isErrorEnabled() && id != null) {
                log.error("Can not resolve response component id " + id);
            }
            return null;
        }
    }

    /**
     * Handles event raisen from a client via AJax. Client can issue an event to
     * the server by<p>
     * url?id=compId&jbRespId=compId
     * <p>along with other event specific
     * parameters.  Event name is passed as value.
     * <p>Named event handler will be invoked on component whose clientid is id. if
     * compId is not null, response is rendered back for component jbRespId.
     * On the client side, element(jbRespId).innerHTML=responseText will render the
     * result.
     *
     * @param ctx
     * @param request
     * @param response
     */
    public
    //JB._e('${context.pageName}.xsp?styleClassId=${pageStyles_cde.row.clientId}', '${pageStyles_cde.row.id}', 'com.jb.web.model.css.event.EditStyleEH.onEditStyle', 'styleDetailsPanel');

    void handleServerEvent(WebContext ctx, HttpServletRequest request, 
                           HttpServletResponse response) {
        long l = System.currentTimeMillis();
        UIComponentImpl component = getRequestComp(ctx, request);
        String version = request.getParameter("version");
        if (version != null) {
            this.handleServerEvent_2(ctx, request, response);
            return;
        }

        if (component != null) {
            ctx.setRequestComponent(component);
            int c;

            try {
                String value = "";
                while (true) {
                    c = request.getInputStream().read();
                    if (c == -1) {
                        break;
                    }
                    value += (char)c;
                }
                //set component value, onValueChange event will be called.
                if ((value != null) && (value.length() > 0)) {
                    EventManager.getInstance().handleEvent(ctx, component, 
                                                           value);
                }
                if (ctx.isError()) {
                    handleError(ctx, response, null);

                    return;
                }
                //render respond back
                if (ctx.getAJaxResponse() == null) {
                    component = getResponseComp(ctx, request);
                    if (component != null) {
                        component.updateView(ctx);

                        String html = 
                            RenderKitImpl.getRenderer(component).renderChildren(ctx, 
                                                                                component).toString();
                        response.getWriter().write(html);
                    }
                } else {
                    response.getWriter().write(ctx.getAJaxResponse());
                }
            } catch (IOException e) {
                handleError(ctx, response, e);
            }
        } else {
            handleError(ctx, response, 
                        new Exception("Component not found " + id));
            return;
        }


        if (log.isDebugEnabled()) {
            log.debug("handleServerEvent took " + 
                      (System.currentTimeMillis() - l));
        }
    }

    private void handleServerEvent_2(WebContext ctx, 
                                     HttpServletRequest request, 
                                     HttpServletResponse response) {
        long l = System.currentTimeMillis();
        UIComponentImpl component = getRequestComp(ctx, request);

        if (component != null) {
            String eventHandler = request.getParameter("eventName");
            ctx.setRequestComponent(component);
            int c;

            try {
                String value = "";
                while (true) {
                    c = request.getInputStream().read();
                    if (c == -1) {
                        break;
                    }

                    value += (char)c;
                }

                AJaxEvent uiEvent = new AJaxEvent(component, request);
                uiEvent.setPayload(value);
                //set component value, onValueChange event will be called.
                if ((value != null) && (value.length() > 0)) {
                    EventManager.getInstance().handleEvent(ctx, uiEvent, 
                                                           eventHandler);
                }
                if (ctx.isError()) {
                    handleError(ctx, response, null);
                    return;
                }
                //render respond back
                if (ctx.getAJaxResponse() == null) {
                    component = getResponseComp(ctx, request);
                    if (component != null) {
                        component.updateView(ctx);

                        String html = 
                            RenderKitImpl.getRenderer(component).renderChildren(ctx, 
                                                                                component).toString();
                        response.getWriter().write(html);
                    }
                } else {
                    response.getWriter().write(ctx.getAJaxResponse());
                }
            } catch (IOException e) {
                handleError(ctx, response, e);
            }
        } else {
            handleError(ctx, response, 
                        new Exception("Component not found " + id));
            return;
        }


        if (log.isDebugEnabled()) {
            log.debug("handleServerEvent took " + 
                      (System.currentTimeMillis() - l));
        }
    }


    /**
     * Handles ajax request for the page, it must restore the tree,
     * and then identify the component for which the request is meant for.
     * <p>
     * Set command to set the property.
     * Render command to render a component, only render its children. The
     * component should be a container itself, e.g. panel, table cell etc.
     * @param ctx context to fulfill the request
     * @param request aJax request from http
     * @param response response to write back, if any
     *
     * @throws IOException If fails to read/parse the income request
     */
    public void processRequest(WebContext ctx, HttpServletRequest request, 
                               HttpServletResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.processRequest");
        }

        if (!load(ctx, request)) {
            response.getWriter().write(ctx.getAllErrors());
        }
        UIComponentImpl page = (UIComponentImpl)ctx.getRespondTree();
        String pageName = (String)properties.get("jb.renderBack.page");
        if (pageName != null) {
            if (!pageName.equals(page.getName()))
                page = ctx.getSession().getPage(ctx, pageName, false);
        }
        if (page != null) {
            page.onRestored(ctx);
        } else {
            handleError(ctx, response, 
                        new Exception("Can not locate page " + pageName));
            return;
        }
        Object component = null;
        if ("session".equals(id)) {
            component = ctx.getSession();
        } else if ("inspector".equals(id)) {
            Page page1 = ctx.getSession().getPage(ctx, "piRegion", false);
            component = page1.getComponentById("propertyInspector");
        } else if ("clientId".equals(idType)) {
            if (id.startsWith("${"))
                try {
                    component = AttributeEvaluator.evaluate(ctx, page, id);
                } catch (ELException e) {
                    if (log.isErrorEnabled())
                        log.error(this, e);
                }
            else
                component = page.getDescendantByClientId(id);
        } else {
            component = page.getDescendantById(id);
        }

        if (component == null) {
            try {
                component = 
                        AttributeEvaluator.evaluate(ctx, page, "${" + id + "}");
            } catch (ELException e) {
                if (log.isErrorEnabled())
                    log.error(this, e);
            }
        }
        //render the component
        if ("com.jb.application.select".equals(this.action)) {
            if (log.isDebugEnabled()) {
                log.debug("Select " + component);
            }
            ctx.getSession().setSelected((UIComponentImpl)component);
            return;
        }
        if ("onDrop".equals(this.action)) {
            if (log.isDebugEnabled()) {
                log.debug("drop " + component);
            }
            String droppedComponentId = (String)properties.get("dropId");


            if (component instanceof Region) {
                DropElementEvent de = 
                    new DropElementEvent(component, droppedComponentId);
                Panel comp = (Panel)component;
                String onDroppedOn = comp.getOnDroppedOn();
                if (onDroppedOn != null) {
                    EventManager.getInstance().handleEvent(ctx, component, 
                                                           onDroppedOn, de);
                }
            }
            return;
        }
        if (component == null) {
            handleError(ctx, response, 
                        new Exception("Can not identify object " + id));
            return;
        }
        if ("com.jb.application.render".equals(action)) {
            if (log.isDebugEnabled()) {
                log.debug("Render " + component);
            }
            renderChildren(ctx, (UIComponentImpl)component);
        }
        if ("invoke".equals(this.action)) {
            if (log.isDebugEnabled()) {
                log.debug("Invoke " + component);
            }
            String renderComp = 
                (String)properties.get("jb.renderBack.component");
            String methodName = (String)properties.get("methodName");
            //we need to know what parameters are there
            String parameters = (String)properties.get("parameters");
            if ((component != null) && (methodName != null)) {
                try {
                    if (parameters == null)
                        PropUtil.invoke(component, methodName);
                    else
                        PropUtil.invoke2(component, methodName, parameters);
                    if (log.isDebugEnabled()) {
                        log.debug("execute " + component + "." + methodName);
                    }
                } catch (ExecutionException e) {
                    if (log.isErrorEnabled())
                        log.error(this, e);
                }
            }
            if (renderComp != null) {
                renderChildren(ctx, 
                               (UIComponentImpl)page.getDescendantByClientId(renderComp));
            } else if (component instanceof UIComponentImpl)
                this.renderComponent(ctx, (UIComponentImpl)component);
        }
        if ("set".equals(this.action)) {
            if (log.isDebugEnabled()) {
                log.debug("Sets property " + component);
            }
            Iterator iter = properties.keySet().iterator();
            String renderComp = null;
            while (iter.hasNext()) {
                String propName = (String)iter.next();
                String propValue = (String)properties.get(propName);
                if ("jb.renderBack.component".equals(propName)) {
                    renderComp = propValue;
                    continue;
                }
                if ("jb.renderBack.page".equals(propName)) {
                    continue;
                }

                if ((component != null) && (propName != null)) {
                    PropUtil.setProperty(component, propName, propValue);
                    if (log.isDebugEnabled()) {
                        log.debug("Setting prop value for " + component + "." + 
                                  propName + "=" + propValue);
                    }
                }
            }
            if (renderComp != null) {
                renderChildren(ctx, 
                               (UIComponentImpl)page.getDescendantByClientId(renderComp));
            } else {
                renderComponent(ctx, (UIComponentImpl)component);
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Done AjaxServer.processRequest");
        }
    }

    /**
     * Handles wizard actions from client. Called by JB.Wizard in jb.ajax.js
     * @param ctx the runtime context
     * @param request the request from the client.
     * @param response the response to send back to the client.
     * @param action the action requested by the client.
     */
    public void wizardAction(WebContext ctx, HttpServletRequest request, 
                             HttpServletResponse response, String action) {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.wizardAction");
        }
        //next
        //cancel
        //submit
        //create
        Session session = ctx.getSession();
        Wizard wizard = (Wizard)session.getAttribute("com.jb.wizard");

        if (wizard != null) {
            UIComponentImpl comp = getRequestComp(ctx, request);
            comp.processDecodes(ctx);
            comp.processValidators(ctx);
            comp.processUpdates(ctx);
            UIEvent e = new UIEvent(comp, request);

            if ("wizard.submit".equals(action))
                wizard.create(ctx, e);
            else if ("wizard.next".equals(action))
                wizard.next(ctx, e);
            else if ("wizard.cancel".equals(action)) {
                wizard.cancel(ctx, e);
                session.removeAttribute("com.jb.wizard");
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("End AjaxServer.wizardAction");
        }
    }

    public void update(WebContext ctx, HttpServletRequest request, 
                       HttpServletResponse response) {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.render");
        }
        try {
            UIComponentImpl page = (UIComponentImpl)ctx.getRequestTree();
            page.onRestored(ctx);

            String respondId = request.getParameter(RES_COMP_ID);

            if (respondId != null) {
                UIComponentImpl component = 
                    (UIComponentImpl)page.getDescendantByClientId(respondId);

                if (component != null) {
                    this.renderChildren(ctx, component);
                }
            }
        } catch (Exception e) {
            handleError(ctx, response, e);
        }
        if (log.isDebugEnabled()) {
            log.debug("Ends AjaxServer.render");
        }
    }

    public void renderComp(WebContext ctx, HttpServletRequest request, 
                           HttpServletResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.renderPanel");
        }
        try {
            String pageName = request.getParameter("pageName");
            String compName = request.getParameter("compName");
            UIComponentImpl page = (UIComponentImpl)ctx.getRequestTree();
            if (pageName != null && pageName.length() != 0) {
                page = ctx.getSession().getPage(ctx, pageName, false);
                ((Page)page).setDialogMode(true);
            }
            page.onRestored(ctx);
            UIComponentImpl component = 
                (UIComponentImpl)page.getDescendantByClientId(compName);
            if (component != null)
                rendererComponent(ctx, component);
            //
            if (pageName != null && pageName.length() != 0) {
                ctx.getSession().addAttribute(RENDERED_PANEL, page);
            }
        } catch (Exception e) {
            handleError(ctx, response, e);
        }
        if (log.isDebugEnabled()) {
            log.debug("Done AjaxServer.renderPanel");
        }
    }

    public void _renderPanel(WebContext ctx, HttpServletRequest request, 
                             HttpServletResponse response) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.renderPanel");
        }
        try {
            String pageName = request.getParameter("pageName");
            String panelName = request.getParameter("panelName");
            if (pageName == null || pageName.length() == 0)
                return;
            Page page = ctx.getSession().getPage(ctx, pageName, false);

            if (page != null) {
                page.onRestored(ctx);
                page.setDialogMode(true);
            } else {
                if (log.isErrorEnabled()) {
                    log.error("Can not find page " + pageName);
                }
                return;
            }

            UIComponentImpl component = 
                (UIComponentImpl)page.getDescendantByClientId(panelName);
            if (component != null)
                rendererComponent(ctx, component);
            ctx.getSession().addAttribute(RENDERED_PANEL, page);
        } catch (Exception e) {
            handleError(ctx, response, e);
        }
        if (log.isDebugEnabled()) {
            log.debug("Done AjaxServer.renderPanel");
        }
    }

    /**
     * Restores the page, and perform all lifecycles.
     * @param ctx
     * @param request
     * @param response
     */
    public void performAll(WebContext ctx, HttpServletRequest request, 
                           HttpServletResponse response) {
        if (log.isDebugEnabled()) {
            log.debug("Starts AjaxServer.performAll");
        }

        try {

            UIComponentImpl form = getRequestComp(ctx, request);
            form.processDecodes(ctx);
            form.processValidators(ctx);
            form.processUpdates(ctx);
            form.updateView(ctx);

            UIComponentImpl responseComp = this.getResponseComp(ctx, request);

            if (responseComp != null) {
                renderChildren(ctx, responseComp);
            }
        } catch (Exception e) {
            handleError(ctx, response, e);
        }

        if (log.isDebugEnabled()) {
            log.debug("Ends AjaxServer.performAll");
        }
    }
    //process load, deserialize the payload into request

    protected boolean process(WebContext ctx, 
                              String xmlString) throws IOException {
        try {
            if (xmlString == null || xmlString.length() == 0)
                return false;
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            dbf.setValidating(false);

            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader is = new StringReader(xmlString);
            Document doc = db.parse(new InputSource(is));
            Node xmlElement = doc.getDocumentElement();
            NodeList nodes = xmlElement.getChildNodes();

            for (int i = 0; i < nodes.getLength(); i++) {
                Node node = nodes.item(i);
                String localName = node.getLocalName();

                if (log.isDebugEnabled()) {
                    log.debug(localName + " = " + 
                              node.getFirstChild().getNodeValue());
                }

                if ("id".equals(localName)) {
                    this.id = node.getFirstChild().getNodeValue();
                } else if ("idType".equals(localName)) {
                    this.idType = node.getFirstChild().getNodeValue();
                } else if ("properties".equals(localName)) {
                    properties = nodeToProperties(node);
                } else if ("action".equals(localName)) {
                    this.action = node.getFirstChild().getNodeValue();
                }
            }
        } catch (Exception e) {
            if (log.isErrorEnabled()) {
                log.error(this, e);
            }
            ctx.addErrorMessage(this, 
                                "Failed to parse income request " + xmlString);
            throw new IOException("Failed to parse income request " + 
                                  xmlString);
        }

        return true;
    }

    private UIComponentImpl getComponent(WebContext ctx, UIComponentImpl page, 
                                         String id) {
        UIComponentImpl component = 
            (UIComponentImpl)page.getDescendantByClientId(id);

        if (page.getClientId().equals(id)) {
            component = page;
        }

        if (component == null) {
            component = (UIComponentImpl)page.getDescendantById(id);
        }
        if (component == null) {
            page = ctx.getToolBar();
            if (page != null) {
                component = (UIComponentImpl)page.getDescendantByClientId(id);
            }
        }
        if (component == null) {
            page = (UIComponentImpl)ctx.getSession().getAttribute(RENDERED_PANEL);
            if (page != null) {
                component = (UIComponentImpl)page.getDescendantByClientId(id);
            }
        }
        return component;
    }
    //called to process payload

    private boolean load(WebContext ctx, 
                         HttpServletRequest request) throws IOException {
        int c;
        String value = "";
        while (true) {
            c = request.getInputStream().read();
            if (c == -1) {
                break;
            }
            value += (char)c;
        }
        if (log.isDebugEnabled()) {
            log.debug(value);
        }
        return process(ctx, value);
    }

    /**
     * Saves property node into a hashmap.
     * @param node
     * @return hashmap as property values.
     */
    private Map nodeToProperties(Node node) {
        HashMap properties = new HashMap();
        NodeList nodes = node.getChildNodes();

        for (int i = 0; i < nodes.getLength(); i++) {
            Element ele = (Element)nodes.item(i);
            String name = ele.getAttribute("name");
            String value = "";
            if (ele.getFirstChild() != null) {
                value = ele.getFirstChild().getNodeValue();
            }
            properties.put(name, value);
        }
        return properties;
    }

    private void renderComponent(WebContext ctx, UIComponentImpl component) {
        try {
            if (component == null) {
                return;
            }
            component.updateView(ctx);
            String html = 
                RenderKitImpl.getRenderer(component).render(ctx, component).toString();
            ctx.getResponse().getWriter().write(html);
        } catch (IOException e) {
            this.handleError(ctx, ctx.getResponse(), e);
        }
    }

    private void renderChildren(WebContext ctx, UIComponentImpl component) {
        try {
            if (component == null) {
                return;
            }
            component.updateView(ctx);
            String html = 
                RenderKitImpl.getRenderer(component).renderChildren(ctx, 
                                                                    component).toString();
            ctx.getResponse().getWriter().write(html);
        } catch (IOException e) {
            this.handleError(ctx, ctx.getResponse(), e);
        }
    }

    private void rendererComponent(WebContext ctx, UIComponentImpl component) {
        try {
            if (component == null) {
                return;
            }
            component.updateView(ctx);
            String html = 
                RenderKitImpl.getRenderer(component).render(ctx, component).toString();
            ctx.getResponse().getWriter().write(html);
        } catch (IOException e) {
            this.handleError(ctx, ctx.getResponse(), e);
        }
    }
}
